@file:OptIn(ExperimentalTime::class)

package com.gitee.wsl.time

import kotlinx.datetime.DatePeriod
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.atTime
import kotlinx.datetime.format
import kotlinx.datetime.format.DayOfWeekNames
import kotlinx.datetime.format.MonthNames
import kotlinx.datetime.format.Padding
import kotlinx.datetime.format.char
import kotlinx.datetime.minus
import kotlinx.datetime.plus
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import kotlin.math.ceil
import kotlin.time.Instant
import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
import kotlin.time.ExperimentalTime

private const val MIN_SECOND: Long = -31557014167219200L
private const val MAX_SECOND: Long = 31556889864403199L

val Instant.epochNanoseconds get() = epochSeconds * T.NANOSECONDS_PER_SECOND + nanosecondsOfSecond

fun Instant.isBefore(other: Instant) = this < other

fun Instant.isAfter(other: Instant) = this > other

/**
 * The earliest supported [Instant], which can be used as a "far past" sentinel.
 */
val Instant.Companion.MIN: Instant
    get() = fromEpochSeconds(MIN_SECOND)

/**
 * The latest supported [Instant], which can be used as a "far future" sentinel.
 */
val Instant.Companion.MAX: Instant
    get() = fromEpochSeconds(MAX_SECOND, 999_999_999L)

/**
 * The [Instant] representing the Unix epoch of 1970-01-01T00:00Z.
 */
val Instant.Companion.UNIX_EPOCH: Instant
    get() =fromEpochSeconds(0L)


/**
 * Returns an Instant of Jan 1st 1970  00:00:00.000 UTC.
 */
//public val Instant.Companion.EPOCH: Instant
//    get() = fromEpochMilliseconds(0)

/**
 * Adds a specified [time] to [this]. Returns a new [Instant]
 */
operator fun Instant.plus(time: LocalTime): Instant = this + time.asDuration

private const val NANOS_LENGTH: Int = 9

/** Render a Kotlin [Instant] as `seconds.nanos`. */
@Suppress("CUSTOM_GETTERS_SETTERS")
val Instant.decimalSeconds: String
    get() {
        val seconds = epochSeconds.toString()
        val nanos = "00000000$nanosecondsOfSecond".let { it.substring(it.length - NANOS_LENGTH) }
        return "$seconds.$nanos"
    }


/**
 * An ISO 8601 UTC string with milliseconds
 * precision. The default [Instant.toString]
 * can be seconds or nanoseconds precision.
 */
fun Instant.toStringMillis(): String {
    return toString().let {
        if (it.length == 20) {
            it.dropLast(1) + ".000Z"
        } else if (it.length > 24) {
            it.dropLast(it.length - 23) + 'Z'
        } else {
            it
        }
    }
}


val Instant.timestampString: String
    get() {
        val timezone = TimeZone.currentSystemDefault()
        val datetime = this.toLocalDateTime(timezone)
        val year = datetime.year
        val month = datetime.monthNumber
        val dayOfMonth = datetime.dayOfMonth

        val hour = datetime.hour
        val minutes = datetime.minute
        val seconds = datetime.second

        return "$year/$month/$dayOfMonth $hour:$minutes:$seconds"
    }

val Instant.month: Int
    get() {
        val timezone = TimeZone.currentSystemDefault()
        val datetime = this.toLocalDateTime(timezone)
        return datetime.monthNumber
    }

fun Long.getMonth(): Int {
    return Instant.fromEpochMilliseconds(this).month
}

fun Long.formatLongTime(): String {
    return Instant.fromEpochMilliseconds(this).timestampString
}

val Instant.Companion.EPOCH: Instant
    get() = Instant.fromEpochMilliseconds( 0L)

/**
 * Parses the provided [isoString] to an [Instant] or null if it cannot be parsed.
 *
 * @see [Instant.parse]
 */
fun Instant.Companion.parseOrNull(isoString: String): Instant? =
    try {
        parse(isoString)
    } catch (exception: Exception) {
        null
    }

fun convertLenientDateTimeStringToInstant(value: String): Instant = Instant.parse(value)

/**
 * Parses the provided [isoString] to an [Instant] and allowing leniency with the provided [String], for example
 * a [String] without some time values.
 */
@ExperimentalTime
fun Instant.Companion.parseLenient(isoString: String): Instant =
    convertLenientDateTimeStringToInstant(value = isoString)

/**
 * Parses the provided [isoString] to an [Instant] or null if it can not be parsed.
 *
 * @see [parseLenient]
 */
@ExperimentalTime
fun Instant.Companion.parseLenientOrNull(isoString: String): Instant? =
    try {
        Instant.parseLenient(isoString)
    } catch (exception: Exception) {
        null
    }

/**
 * Retrieves a [Duration] of the time elapsed since the Epoch.
 *
 * @see [Instant.toEpochMilliseconds]
 */
@ExperimentalTime
val Instant.durationSinceEpoch: Duration
    get() = toEpochMilliseconds().milliseconds

/**
 * Retrieves the [Duration] between this [Instant] and the provided [other] [Instant].
 *
 * If the [other] [Instant] is later in time than this [Instant], the returned [Duration] will be positive.
 */
@ExperimentalTime
infix fun Instant.durationTo(other: Instant): Duration =
    (other.toEpochMilliseconds() - toEpochMilliseconds()).milliseconds

/**
 * Converts this [Duration] since the Epoch to an [Instant].
 */
@ExperimentalTime
fun Duration.toInstantSinceEpoch(): Instant =
    Instant.fromEpochMilliseconds(this.inWholeMilliseconds)


/**
 * Converts this [Long] milliseconds since the Epoch value to an [Instant].
 */
@ExperimentalTime
fun Long.toInstantFromMillisecondsSinceEpoch(): Instant = milliseconds.toInstantSinceEpoch()


// Time-zone aware conversion utils

fun Instant.toSystemDefaultLocalDateTime(): LocalDateTime {
    val currentZone = TimeZone.currentSystemDefault()
    return toLocalDateTime(timeZone = currentZone)
}

fun Instant.toSystemDefaultLocalDate(): LocalDate {
    val currentZone = TimeZone.currentSystemDefault()
    return toLocalDateTime(timeZone = currentZone).date
}

fun LocalDateTime.toSystemDefaultTimeZoneInstant(): Instant {
    val currentZone = TimeZone.currentSystemDefault()
    return toInstant(currentZone)
}

// Date time computation utils

/***
 * Converts this instant to the ISO 8601 string representation without seconds, for example, 2023-01-02T23:40Z.
 */
fun Instant.toIso8601WithoutSeconds(): String {
    val formatted = toString()
    // This assumes the format is always compliant with ISO-8601 extended format.
    return formatted.substring(0, formatted.lastIndexOf(':')) + "Z"
}


fun Instant.atStartOfHour(): Instant {
    val currentLocalDateTime = toSystemDefaultLocalDateTime()
    return LocalDateTime(
        date = currentLocalDateTime.date,
        time = LocalTime(
            hour = currentLocalDateTime.hour,
            minute = 0,
        ),
    ).toSystemDefaultTimeZoneInstant()
}

fun Instant.atStartOfDay(): Instant {
    val currentLocalDate = toSystemDefaultLocalDateTime().date
    return currentLocalDate.atStartOfDayIn(timeZone = TimeZone.currentSystemDefault())
}

fun Instant.atEndOfDay(): Instant {
    val currentLocalDate = toSystemDefaultLocalDateTime().date
    return currentLocalDate
        .plus(period = DatePeriod(days = 1))
        .atStartOfDayIn(timeZone = TimeZone.currentSystemDefault())
        .minus(value = 1, unit = DateTimeUnit.NANOSECOND)
}

fun Instant.getDayRange(): ClosedRange<Instant> = atStartOfDay()..atEndOfDay()

fun Instant.getWeekRange(): ClosedRange<Instant> {
    val localDateTime = toSystemDefaultLocalDateTime()
    return localDateTime.startOfWeek()..localDateTime.endOfWeek()
}

fun Instant.getMonthRange(): ClosedRange<Instant> {
    val localDateTime = toSystemDefaultLocalDateTime()
    return localDateTime.startOfMonth()..localDateTime.endOfMonth()
}

fun Instant.getYearRange(): ClosedRange<Instant> {
    val localDateTime = toSystemDefaultLocalDateTime()
    return localDateTime.startOfYear()..localDateTime.endOfYear()
}

/***
 * Returns the number of milliseconds to the upcoming 00 or 30 minutes.
 * This is for recurrent background tasks.
 */
fun Instant.getNextHalfHourCountdownMillis(): Long {
    val currentLocalDateTime = toSystemDefaultLocalDateTime()
    val currentMinute = currentLocalDateTime.minute
    val currentSecond = currentLocalDateTime.second
    val currentNanosecond = currentLocalDateTime.nanosecond

    val minutesToNextTarget = if (currentMinute < 30) {
        30 - currentMinute
    } else {
        60 - currentMinute
    }
    val delayInMillis = (minutesToNextTarget * 60 * 1000) - (currentSecond * 1000) - (currentNanosecond / 1_000_000)

    return delayInMillis.toLong()
}

/***
 * Returns English abbreviated day of week name and day of month with padding. Example: Mon 02
 */
fun Instant.getLocalDayOfWeekAndDayString(): String {
    val localDate = toSystemDefaultLocalDate()
    val customFormat = LocalDate.Format {
        dayOfWeek(DayOfWeekNames.ENGLISH_ABBREVIATED)
        char(' ')
        dayOfMonth(padding = Padding.ZERO)
    }
    return localDate.format(customFormat)
}

/***
 * Returns local day of month with padding and English abbreviated month. Example: 02 Feb
 */
fun Instant.getLocalDayMonthString(): String {
    val localDate = toSystemDefaultLocalDate()
    val customFormat = LocalDate.Format {
        dayOfMonth(padding = Padding.ZERO)
        char(' ')
        monthName(MonthNames.ENGLISH_ABBREVIATED)
    }
    return localDate.format(customFormat)
}

/***
 * Returns local month in abbreviated English
 */
fun Instant.getLocalMonthString(): String {
    val localDate = toSystemDefaultLocalDate()
    val customFormat = LocalDate.Format {
        monthName(MonthNames.ENGLISH_ABBREVIATED)
    }
    return localDate.format(customFormat)
}

/***
 * Returns local month in abbreviated English and year. Example: Jan 2024
 */
fun Instant.getLocalMonthYearString(): String {
    val localDate = toSystemDefaultLocalDate()
    val customFormat = LocalDate.Format {
        monthName(MonthNames.ENGLISH_ABBREVIATED) // Full month name
        char(' ')
        year()
    }
    return localDate.format(customFormat)
}

fun ClosedRange<Instant>.getHalfHourlyTimeSlotCount(): Int {
    // adjust the start time to 00/30 minutes
    val adjustedStartTime = adjustToNext00Or30(start.epochSeconds)

    if (endInclusive.epochSeconds <= adjustedStartTime) return 0

    val durationInSeconds = endInclusive.epochSeconds - adjustedStartTime
    return ceil(durationInSeconds / 1800.0).toInt() + 1
}

private fun adjustToNext00Or30(epochSeconds: Long): Long {
    val minutes = (epochSeconds / 60) % 60
    val adjustment = when {
        minutes < 30 -> 30 - minutes
        else -> 60 - minutes
    }
    return epochSeconds + adjustment * 60
}


/***
 * Agile Unit Rates up to 23:00 the next day are published around 16:00 every day.
 * It is no use requesting anything we know it's not available.
 */
fun Instant.getAgileClosingTime(): Instant {
    val londonTimeZone = TimeZone.of("Europe/London")
    val londonDateTime = this.toLocalDateTime(londonTimeZone)
    val fifteenOClockToday = londonDateTime.date.atTime(hour = 15, minute = 0)
    val closingTimeToday = londonDateTime.date.atTime(hour = 23, minute = 0)

    if (londonDateTime < fifteenOClockToday) {
        return closingTimeToday.toInstant(londonTimeZone)
    }

    val closingTimeTomorrow = (londonDateTime.date + DatePeriod(days = 1)).atTime(23, 0)
    return closingTimeTomorrow.toInstant(londonTimeZone)
}


/**
 * Returns this instant, rounded to match the precision of a given [unit]. If the time is halfway between whole values
 * of the unit, it will be rounded up.
 */
fun Instant.roundedTo(unit: DurationUnit): Instant {
    return when (unit) {
        DurationUnit.DAYS -> roundedToNearestSecondsUnchecked(24.hours)
        DurationUnit.HOURS -> roundedToNearestSecondsUnchecked(1.hours)
        DurationUnit.MINUTES -> roundedToNearestSecondsUnchecked(1.minutes)
        DurationUnit.SECONDS -> roundedToNearestSecondsUnchecked(1.seconds)
        DurationUnit.MILLISECONDS -> roundedToNearestUnchecked(1_000_000.nanoseconds)
        DurationUnit.MICROSECONDS -> roundedToNearestUnchecked(1_000.nanoseconds)
        DurationUnit.NANOSECONDS -> this
        else -> this
    }
}

private fun floorMod(var0: Long, var2: Long): Long {
    val var4 = var0 % var2
    return if ((var0 xor var2) < 0L && var4 != 0L) var4 + var2 else var4
}

internal fun Instant.roundedToNearestSecondsUnchecked(increment: Duration): Instant {

    val remainder = floorMod(epochSeconds,increment.inWholeSeconds).seconds + nanosecondsOfSecond.nanoseconds

    return if (remainder > Duration.ZERO) {
        if (remainder < increment / 2) {
            this - remainder
        } else {
            this + (increment - remainder)
        }
    } else {
        this
    }
}

internal fun Instant.roundedToNearestUnchecked(increment: Duration): Instant {
    val remainder = nanosecondsOfSecond.nanoseconds % increment

    return if (remainder > Duration.ZERO) {
        if (remainder < increment / 2) {
            this - remainder
        } else {
            this + (increment - remainder)
        }
    } else {
        this
    }
}

/**
 * Returns this instant, rounded down to match the precision of a given [unit].
 *
 * This is equivalent to [truncatedTo].
 */
fun Instant.roundedDownTo(unit: DurationUnit): Instant {
    return when (unit) {
        DurationUnit.DAYS -> roundedDownToNearestSecondsUnchecked(24.hours)
        DurationUnit.HOURS -> roundedDownToNearestSecondsUnchecked(1.hours)
        DurationUnit.MINUTES -> roundedDownToNearestSecondsUnchecked(1.minutes)
        DurationUnit.SECONDS -> roundedDownToNearestSecondsUnchecked(1.seconds)
        DurationUnit.MILLISECONDS -> roundedDownToNearestUnchecked(1_000_000.nanoseconds)
        DurationUnit.MICROSECONDS -> roundedDownToNearestUnchecked(1_000.nanoseconds)
        DurationUnit.NANOSECONDS -> this
        else -> this
    }
}

internal fun Instant.roundedDownToNearestSecondsUnchecked(increment: Duration): Instant {

    val remainder = floorMod(epochSeconds, increment.inWholeSeconds)

    return if (remainder > 0 || nanosecondsOfSecond > 0) {
        Instant.fromEpochSeconds(epochSeconds - remainder, 0)
    } else {
        this
    }
}

internal fun Instant.roundedDownToNearestUnchecked(increment: Duration): Instant {
    val remainder = nanosecondsOfSecond.nanoseconds % increment
    return if (remainder > Duration.ZERO) this - remainder else this
}


/**
 * Returns this instant, rounded up to match the precision of a given [unit].
 */
fun Instant.roundedUpTo(unit: DurationUnit): Instant {
    return when (unit) {
        DurationUnit.DAYS -> roundedUpToNearestSecondsUnchecked(24.hours)
        DurationUnit.HOURS -> roundedUpToNearestSecondsUnchecked(1.hours)
        DurationUnit.MINUTES -> roundedUpToNearestSecondsUnchecked(1.minutes)
        DurationUnit.SECONDS -> roundedUpToNearestSecondsUnchecked(1.seconds)
        DurationUnit.MILLISECONDS -> roundedUpToNearestUnchecked(1_000_000.nanoseconds)
        DurationUnit.MICROSECONDS -> roundedUpToNearestUnchecked(1_000.nanoseconds)
        DurationUnit.NANOSECONDS -> this
        else -> this
    }
}


internal fun Instant.roundedUpToNearestSecondsUnchecked(increment: Duration): Instant {
    //val remainder = (secondOfUnixEpoch floorMod increment.value).seconds + additionalNanosecondsSinceUnixEpoch
    val remainder = floorMod(epochSeconds, increment.inWholeSeconds).seconds + nanosecondsOfSecond.nanoseconds

    return if (remainder > Duration.ZERO) this + (increment - remainder) else this
}

internal fun Instant.roundedUpToNearestUnchecked(increment: Duration): Instant {
    val remainder = nanosecondsOfSecond.nanoseconds % increment
    return if (remainder > Duration.ZERO) this + (increment - remainder) else this
}

/** Returns an Emoji representation of this [Instant] and the specified [roundingMode]. */
//fun Instant.asEmoji(): String = when (utcMinutes) {
//    in 0..14 -> fullHourClocks[utcHours.mod(12)]
//    in 15..44 -> halfHourClocks[utcHours.mod(12)]
//    else -> fullHourClocks[(utcHours + 1).mod(12)]
//}
//
//private val fullHourClocks = listOf("🕛", "🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚")
//private val halfHourClocks = listOf("🕧", "🕜", "🕝", "🕞", "🕟", "🕠", "🕡", "🕢", "🕣", "🕤", "🕥", "🕦")
